Repository navigation
feat(api): assign explicit errorCodes to all validators + coverage guard (#231) - #257
Conversation
…ard (#231) Follow-up to #45, which shipped the additive explicit-only errorCodes contract but coded only the /me/language validator. This assigns a stable Feature.Field.Rule code to every rule in the remaining 34 FluentValidation validators (219 rules), so the SPA (#182) can key translated validation messages off the code with the English message as fallback. - Additive only: no WithMessage text, rule logic, or When/Unless conditions changed — the "errors" dictionary is byte-for-byte unchanged (verified by 105 Application + 427 integration tests, all green). errorCodes now appears on 400s that previously carried none. - Naming: Feature.Field.Rule, PascalCase, one internal triple so every code contains a '.' (the discriminator ValidationResponse uses to tell an explicit code from a FluentValidation default). Me.* for self-service (change-own- password, language), User.* for admin user ops, FarmSettings.* for settings, InventoryAdjustment / InventoryPurchase / FeedUsage / WaterUsage for the inventory movements. - Guard: ValidatorErrorCodeCoverageTests reflects over every AbstractValidator in Cluckwork.Application and asserts every rule component carries an explicit dotted code — so a new rule added without WithErrorCode fails the build. Child-validator adaptors (RuleForEach/ChildRules wrappers) are skipped (they emit no message of their own; their leaf rules are coded individually). - ErrorCodesContractTests flipped: CreateUser blank-email now emits User.Email.Required; the "no explicit code -> errorCodes omitted" branch stays covered as a pure-function case in ValidationResponseTests.
Review (pi) caught a hole: the coverage guard SKIPPED IChildValidatorAdaptor components, so the inline RuleForEach(...).ChildRules leaf rules (DailyEntry Grades[].EggGradeId / .Quantity) were never examined — a new inner rule added without a code would not have failed the build, defeating #231 for exactly that pattern. Recurse THROUGH the adaptor into its fixed child validator (reflected from the adaptor's private field, since an inline ChildRules validator is not a discoverable type) rather than skipping it. Provider-based SetValidator(ctx=>…) has no static instance but those are named AbstractValidator<> types already discovered independently. Cycle-guarded. Verified by mutation: removing a child rule's WithErrorCode now fails the guard with "Quantity -> ''"; it passed before. Guard still 35/35 green with all codes present.
…idators (#231) Addresses codex + adversarial review of the ChildRules recursion: - Fail closed: if GetChildValidator cannot reach an adaptor's child (a provider-based SetValidator(ctx=>...), or a FluentValidation change that breaks the private-field reflection), emit an uncoded sentinel instead of silently continuing — so an un-inspectable child can never quietly hide uncoded rules. None exist today, so the guard stays 36/36 green. - Fail loud: a new fact asserts a known inline-ChildRules leaf code is actually reached, so if a FluentValidation upgrade renames the private field the recursion depends on, the guard fails here (and via fail-closed in the DailyEntry theory cases) instead of silently reopening the #231 hole.
Review — codex + 2 Claude agents + pi (all findings resolved)
One substantive finding — independently raised by all three of pi, codex, and the adversarial pass, then fixed in follow-up commits:
Ruled out by the adversarial pass (all confirmed against source): every Final state: additive-only ( Non-blocking note (codex): branch name |
Closes #231 (follow-up to #45). Assigns a stable
Feature.Field.Rulecode to every rule in all 34 remaining FluentValidation validators (~220 codes), so the SPA (#182) can key translated validation messages off a machine code with the English message as fallback.Additive, message-preserving
The
errorsdictionary is byte-for-byte unchanged — only.WithErrorCode(...)calls were appended; noWithMessagetext, rule logic, orWhen/Unlesscondition was touched.errorCodesnow appears on 400s that previously carried none (it stays absent only for non-FluentValidation 400s).Verified: build clean · guard 35/35 · Application 105/105 · Integration 427/427.
Naming convention
<Feature>.<Field>.<Rule>, PascalCase, exactly one internal triple so every code contains a.— the discriminator ValidationResponse uses to tell an explicit code from a FluentValidation default (NotEmptyValidatoretc.). Decisions:Me.*for self-service (change-own-password, language — mirrors the i18n infrastructure (API half): validation error codes + users.language + GET/PATCH /me #45Me.Language.Formatreference);User.*for admin user ops (distinct:User.Password.*vsMe.NewPassword.*).FarmSettings.*for settings;InventoryAdjustment/InventoryPurchase/FeedUsage/WaterUsagefor the inventory movement commands;Payment/SalesOrder/OrderItemfor sales; entity noun elsewhere.The guard (prevents re-drift)
ValidatorErrorCodeCoverageTestsreflects over everyAbstractValidator<>inCluckwork.Applicationand asserts every rule component carries an explicit dotted code — so a new rule added without.WithErrorCode(...)fails the build. Written test-first (it failed listing every uncoded rule, then went green as codes landed). It recurses through child-validator adaptors (RuleForEach(...).ChildRules/.SetValidator) into their leaf rules — an inlineChildRulesvalidator is not a discoverable type, so the recursion (reflecting the adaptor's fixed child validator) is what guards those rules. If a child can't be statically reached it fails closed (flags it), and a dedicated fact asserts a knownChildRulesleaf code is actually reached so a future FluentValidation change can't silently reopen the gap. (This recursion + fail-closed hardening — commits after the initial one — resolves the review finding that pi, codex, and the adversarial pass all independently raised.)Test flip
ErrorCodesContractTestsasserted CreateUser-blank-email emits noerrorCodes— impossible now that CreateUser is coded. Flipped to assert it emitsUser.Email.Required. The complementary "no explicit code →errorCodesomitted" branch stays covered as a pure-function case inValidationResponseTests.Default_framework_code_never_leaks.Not included
Purely additive on the server; the SPA catalog does not yet map these new codes (English fallback still renders) — mapping them to translated
errorskeys is downstream #182 work. Unrelated local working-tree edits are not in this branch.